namespace Game {
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;

    using UnityEngine;
#if UNITY_EDITOR
    using UnityEditor;
#endif

    using Newtonsoft.Json;

    public class Assets : MonoBehaviour {
        public static Assets Ins { get; private set; }

        #region Init

        public static IEnumerator OnInit(AssetBundleManifest manifest) {
            if (Ins != null) yield break;

            var proxy = new GameObject("AssetsProxy");
            Ins = proxy.AddComponent<Assets>();
            DontDestroyOnLoad(proxy);
            yield return null;

            if (manifest != null) {
                Ins.manifest = manifest;

                string[] names = new string[] { "lua.u", "settings.u", "fonts.u" };
                foreach (var n in names) yield return LoadAsync<AssetBundle>(n, null, Cache.Mode.WithApplication);

                yield return LoadAsync<AssetBundle>("a2b.u");

                AssetBundle bundle = Ins.bundles.Get<AssetBundle>("a2b.u");
                if (bundle != null) {
                    var req = bundle.LoadAssetAsync<TextAsset>(Dir.A2BPath);
                    yield return req;

                    if (!req.isDone || req.asset == null) {
                        Logger.Error("Assets.OnInit miss a2b.json.");
                        yield break;
                    }

                    Ins.a2b = JsonConvert.DeserializeObject<Dictionary<string, string>>((req.asset as TextAsset).text);
                }
            }
        }

        #endregion

        #region Load

        public static T Load<T>(string name, Cache.Mode mode = Cache.Mode.Temp5s) where T : Object {
            if (typeof(T) == typeof(AssetBundle))
                return Ins.InnerLoadBundle(name, mode) as T;
            else
                return Ins.InnerLoadAsset<T>(name, mode);
        }

        public static IEnumerator LoadAsync<T>(string name, System.Action<T> onFinished = null, Cache.Mode mode = Cache.Mode.Temp5s) where T : Object {
            if (typeof(T) == typeof(AssetBundle))
                yield return Ins.InnerLoadBundleAsync(name, o => {
                    if (onFinished != null) onFinished.Invoke(o as T);
                }, mode);
            else
                yield return Ins.InnerLoadAssetAsync<T>(name, onFinished, mode);
        }

        #endregion

        #region Instantiate

        public static IEnumerator InstantiateAsync<T>(string name, System.Action<T> onFinished = null, Cache.Mode mode = Cache.Mode.Temp5s) where T : Object {
            yield return InstantiateAsync<T>(name, Vector3.zero, Quaternion.identity, null, onFinished, mode);
        }

        public static IEnumerator InstantiateAsync<T>(string name,
            Vector3 pos, Quaternion qua,
            System.Action<T> onFinished = null, Cache.Mode mode = Cache.Mode.Temp5s) where T : Object {
            yield return InstantiateAsync<T>(name, pos, qua, null, onFinished, mode);
        }

        public static IEnumerator InstantiateAsync<T>(
            string name,
            Vector3 pos, Quaternion qua, Transform parent,
            System.Action<T> onFinished = null, Cache.Mode mode = Cache.Mode.Temp5s) where T : Object {
            T prefab = null; yield return LoadAsync<T>(name, o => prefab = o, mode); if (prefab == null) yield break;
            T obj = Instantiate<T>(prefab, pos, qua, parent);
            if (onFinished != null) onFinished(obj);
        }

        #endregion

        #region Private Functions

        private AssetBundle InnerLoadBundle(string name, Cache.Mode mode) {
            var find = bundles.Get<AssetBundle>(name);
            if (find != null) return find;

            string path = Dir.Storage + name;
            if (!File.Exists(path)) {
                Logger.Error("Assets.LoadBundle missing bundle: {0}", name);
                return null;
            }

            string[] deps = manifest.GetAllDependencies(name);
            foreach (var d in deps) InnerLoadBundle(d, mode);

            find = AssetBundle.LoadFromFile(path);
            bundles.Add(name, find, mode);

            return find;
        }

        private IEnumerator InnerLoadBundleAsync(string name, System.Action<AssetBundle> onFinished, Cache.Mode mode) {
            if (bundles.Get<AssetBundle>(name) != null) yield break;
            
            string path = Dir.Storage + name;

            if (!File.Exists(path)) {
                Logger.Error("Assets.LoadBundleAsync missing bundle: {0}", path);
                yield break;
            }

            string[] deps = manifest.GetAllDependencies(name);
            foreach (var d in deps) yield return InnerLoadBundleAsync(d, null, mode);

            var req = AssetBundle.LoadFromFileAsync(path);
            yield return req;

            var bundle = req.assetBundle;
            if (bundle == null) {
                Logger.Error("Assets.LoadBundleAsync broken: {0}", name);
            } else {
                bundles.Add(name, bundle, mode);
                if (onFinished != null) onFinished(bundle);
            }
        }

        private T InnerLoadAsset<T>(string name, Cache.Mode mode) where T : Object {
            if (string.IsNullOrEmpty(name)) return null;

            var fullPath = Dir.RealPathInBundle<T>(name);

            T find = assets.Get<T>(fullPath);
            if (find != null) return find;

            string bundleName;
            if (a2b.TryGetValue(fullPath, out bundleName)) {
                var bundle = bundles.Get<AssetBundle>(bundleName);

                if (bundle == null) bundle = InnerLoadBundle(bundleName, mode);
                if (bundle == null) {
                    Logger.Error("Assets.LoadAsset failed to load bundle {0} for asset {1}", bundleName, name);
                    return null;
                }

                find = bundle.LoadAsset<T>(fullPath);
                assets.Add(fullPath, find, mode);
                return find;
            } else {
#if UNITY_EDITOR
                if (File.Exists(fullPath)) {
                    find = AssetDatabase.LoadAssetAtPath<T>(fullPath);
                    return find;
                }
#endif
                var subPath = Path.GetDirectoryName(fullPath) + "/" + Path.GetFileNameWithoutExtension(fullPath);
                find = Resources.Load<T>(subPath);
                if (find == null) {
                    Logger.Error("Assets.LoadAsset failed to load asset: {0}", name);
                }
                assets.Add(fullPath, find, mode);
                return find;
            }
        }

        private IEnumerator InnerLoadAssetAsync<T>(string name, System.Action<T> onFinished, Cache.Mode mode) where T : Object {
            if (string.IsNullOrEmpty(name)) yield break;

            var fullPath = Dir.RealPathInBundle<T>(name);
            
            T find = assets.Get<T>(fullPath);
            if (find != null) {
                if (onFinished != null) onFinished.Invoke(find);
                yield break;
            }

            string bundleName;
            if (a2b.TryGetValue(fullPath, out bundleName)) {
                var bundle = bundles.Get<AssetBundle>(bundleName);

                if (bundle == null) yield return InnerLoadBundleAsync(bundleName, o => bundle = o, mode);
                if (bundle == null) {
                    Logger.Error("Assets.LoadAsset failed to load bundle {0} for asset {1}", bundleName, name);
                    yield break;
                }

                var req = bundle.LoadAssetAsync<T>(fullPath);
                yield return req;
                assets.Add(fullPath, req.asset, mode);
                if (onFinished != null) onFinished.Invoke(req.asset as T);
            } else {
#if UNITY_EDITOR
                if (File.Exists(fullPath)) {
                    find = AssetDatabase.LoadAssetAtPath<T>(fullPath);
                    if (onFinished != null) onFinished.Invoke(find);
                    yield break;
                }
#endif
                var subPath = Path.GetDirectoryName(fullPath) + "/" + Path.GetFileNameWithoutExtension(fullPath);
                var req = Resources.LoadAsync<T>(subPath);
                yield return req;
                if (req.asset == null) {
                    Logger.Error("Assets.LoadAssetAsync failed to load asset: {0}", name);
                }
                assets.Add(fullPath, req.asset, mode);
                if (onFinished != null) onFinished.Invoke(req.asset as T);
            }
        }

        private void Awake() {
            assets  = gameObject.AddComponent<Cache>();
            bundles = gameObject.AddComponent<Cache>();

            bundles.onUnload.AddListener(o => { (o as AssetBundle).Unload(false); });
        }

        #endregion

        private AssetBundleManifest manifest = null;
        private Dictionary<string, string> a2b = new Dictionary<string, string>();
        private Cache bundles = null;
        private Cache assets = null;
    }
}
